/**
 *
 * http://bl.ocks.org/benjchristensen/raw/2657838/line-graph.js
 *
 * Create and draw a new line-graph.
 * 
 * Arguments:
 *	 containerId => id of container to insert SVG into [REQUIRED]
 *	 marginTop => Number of pixels for top margin. [OPTIONAL => Default: 20]
 *	 marginRight => Number of pixels for right margin. [OPTIONAL => Default: 20]
 *	 marginBottom => Number of pixels for bottom margin. [OPTIONAL => Default: 35]
 *	 marginLeft => Number of pixels for left margin. [OPTIONAL => Default: 90]
 *	 data => a dictionary containing the following keys [REQUIRED]
 *		 values => The data array of arrays to graph. [REQUIRED]
 *		 start => The start time in milliseconds since epoch of the data. [REQUIRED]
 *		 end => The end time in milliseconds since epoch of the data. [REQUIRED]
 *		 step => The time in milliseconds between each data value.	 [REQUIRED]	
 *		 names => The metric name for each array of data. [REQUIRED]
 *		 displayNames => Display name for each metric. [OPTIONAL => Default: same as 'names' argument]
 *				Example: ['MetricA', 'MetricB'] 
 *		 axis => Which axis (left/right) to put each metric on. [OPTIONAL => Default: Display all values on single axis]
 *				Example: ['left', 'right', 'right'] to display first metric on left axis, next two on right axis.
 *		 colors => What color to use for each metric. [OPTIONAL => Default: black]
 *				Example: ['blue', 'red'] to display first metric in blue and second in red.
 *		 scale => What scale to display the graph with. [OPTIONAL => Default: linear]
 *				Possible Values: linear, pow, log
 *		 rounding => How many decimal points to round each metric to. [OPTIONAL => Default: Numbers are rounded to whole numbers (0 decimals)]
 *				Example: [2, 1] to display first metric with 2 decimals and second metric with 1. 
 *		 numAxisLabelsPowerScale => Hint for how many labels should be displayed for the Y-axis in Power scale. [OPTIONAL => Default: 6]
 *		 numAxisLabelsLinearScale  => Hint for how many labels should be displayed for the Y-axis in Linear scale. [OPTIONAL => Default: 6]
 *
 * Events (fired from container):
 *	 LineGraph:dataModification => whenever data is changed
 *	 LineGraph:configModification => whenever config is changed
 */
function LineGraph(argsMap) {
	/* *************************************************************** */
	/* public methods */
	/* *************************************************************** */
	var self = this;
	
	/**
	 * This allows appending new data points to the end of the lines and sliding them within the time window:
	 * - x-axis will slide to new range
	 * - new data will be added to the end of the lines
	 * - equivalent number of data points will be removed from beginning of lines
	 * - lines will be transitioned through horizontoal slide to show progression over time
	 */
	this.slideData = function(newData) {
		// validate data
		var tempData = processDataMap(newData);
		debug("Existing startTime: " + data.startTime + "  endTime: " + data.endTime);
		debug("New startTime: " + tempData.startTime + "  endTime: " + tempData.endTime);
		
		// validate step is the same on each
		if(tempData.step != newData.step) {
			throw new Error("The step size on appended data must be the same as the existing data => " + data.step + " != " + tempData.step);
		}

		if(tempData.values[0].length == 0) {
			throw new Error("There is no data to append.");
		}
		
		var numSteps = tempData.values[0].length;
		console.log("slide => add num new values: " + numSteps);
		console.log(tempData.values[0])
		tempData.values.forEach(function(dataArrays, i) {
			var existingDataArrayForIndex = data.values[i];
			dataArrays.forEach(function(v) {
				console.log("slide => add new value: " + v);
				// push each new value onto the existing data array
				existingDataArrayForIndex.push(v);
				// shift the front value off to compensate for what we just added
				existingDataArrayForIndex.shift();
			})
		})
		
		// shift domain by number of data elements we just added
		// == numElements * step
		data.startTime = new Date(data.startTime.getTime() + (data.step * numSteps));
		data.endTime = tempData.endTime;
		debug("Updated startTime: " + data.startTime + "  endTime: " + data.endTime);
				
		/*
		* The following transition implementation was learned from examples at http://bost.ocks.org/mike/path/
		* In particular, view the HTML source for the last example on the page inside the tick() function.
		*/

		// redraw each of the lines
			// Transitions are turned off on this since the small steps we're taking
			// don't actually look good when animated and it uses unnecessary CPU
			// The quick-steps look cleaner, and keep the axis/line in-sync instead of jittering
		redrawAxes(false);	
		redrawLines(false);
			
	    // slide the lines left
	    graph.selectAll("g .lines path")
	        .attr("transform", "translate(-" + x(numSteps*data.step) + ")");
		 
		handleDataUpdate();
		
		// fire an event that data was updated
		$(container).trigger('LineGraph:dataModification')
	}
	
	/**
	 * This does a full refresh of the data:
	 * - x-axis will slide to new range
	 * - lines will change in place
	 */
	this.updateData = function(newData) {
		// data is being replaced, not appended so we re-assign 'data'
		data = processDataMap(newData);
		// and then we rebind data.values to the lines
	    graph.selectAll("g .lines path").data(data.values)
		
		// redraw (with transition)
		redrawAxes(true);
		// transition is 'false' for lines because the transition is really weird when the data significantly changes
		// such as going from 700 points to 150 to 400
		// and because of that we rebind the data anyways which doesn't work with transitions very well at all
		redrawLines(false);
		
		handleDataUpdate();
		
		// fire an event that data was updated
		$(container).trigger('LineGraph:dataModification')
	}

	
	this.switchToPowerScale = function() {
		yScale = 'pow';
		redrawAxes(true);
		redrawLines(true);
		
		// fire an event that config was changed
		$(container).trigger('LineGraph:configModification')
	}

	this.switchToLogScale = function() {
		yScale = 'log';
		redrawAxes(true);
		redrawLines(true);
		
		// fire an event that config was changed
		$(container).trigger('LineGraph:configModification')
	}

	this.switchToLinearScale = function() {
		yScale = 'linear';
		redrawAxes(true);		
		redrawLines(true);
		
		// fire an event that config was changed
		$(container).trigger('LineGraph:configModification')
	}
	
	/**
	 * Return the current scale value: pow, log or linear
	 */
	this.getScale = function() {
		return yScale;
	}

	
	
	/* *************************************************************** */
	/* private variables */
	/* *************************************************************** */
	// the div we insert the graph into
	var containerId;
	var container;
	
	// functions we use to display and interact with the graphs and lines
	var graph, x, yLeft, yRight, xAxis, yAxisLeft, yAxisRight, yAxisLeftDomainStart, linesGroup, linesGroupText, lines, lineFunction, lineFunctionSeriesIndex = -1;
	var yScale = 'linear'; // can be pow, log, linear
	var scales = [['linear','Linear'], ['pow','Power'], ['log','Log']];
	var hoverContainer, hoverLine, hoverLineXOffset, hoverLineYOffset, hoverLineGroup;
	var legendFontSize = 12; // we can resize dynamically to make fit so we remember it here

	// instance storage of data to be displayed
	var data;
		
	// define dimensions of graph
	var margin = [-1, -1, -1, -1]; // margins (top, right, bottom, left)
	var w, h;	 // width & height
	
	var transitionDuration = 300;
	
	var formatNumber = d3.format(",.0f") // for formatting integers
	var tickFormatForLogScale = function(d) { return formatNumber(d) };
	
	// used to track if the user is interacting via mouse/finger instead of trying to determine
	// by analyzing various element class names to see if they are visible or not
	var userCurrentlyInteracting = false;
	var currentUserPositionX = -1;
		
	/* *************************************************************** */
	/* initialization and validation */
	/* *************************************************************** */
	var _init = function() {
		// required variables that we'll throw an error on if we don't find
		containerId = getRequiredVar(argsMap, 'containerId');
		container = document.querySelector('#' + containerId);
		
		// margins with defaults (do this before processDataMap since it can modify the margins)
		margin[0] = getOptionalVar(argsMap, 'marginTop', 20) // marginTop allows fitting the actions, date and top of axis labels
		margin[1] = getOptionalVar(argsMap, 'marginRight', 20)
		margin[2] = getOptionalVar(argsMap, 'marginBottom', 35) // marginBottom allows fitting the legend along the bottom
		margin[3] = getOptionalVar(argsMap, 'marginLeft', 90) // marginLeft allows fitting the axis labels
		
		// assign instance vars from dataMap
		data = processDataMap(getRequiredVar(argsMap, 'data'));
		
		/* set the default scale */
		yScale = data.scale;

		// do this after processing margins and executing processDataMap above
		initDimensions();
		
		createGraph()
		//debug("Initialization successful for container: " + containerId)	
		
		// window resize listener
		// de-dupe logic from http://stackoverflow.com/questions/667426/javascript-resize-event-firing-multiple-times-while-dragging-the-resize-handle/668185#668185
		var TO = false;
		$(window).resize(function(){
		 	if(TO !== false)
		    	clearTimeout(TO);
		 	TO = setTimeout(handleWindowResizeEvent, 200); // time in miliseconds
		});
	}
	
	
	
	/* *************************************************************** */
	/* private methods */
	/* *************************************************************** */

	/*
	 * Return a validated data map
	 * 
	 * Expects a map like this:
	 *	 {"start": 1335035400000, "end": 1335294600000, "step": 300000, "values": [[28,22,45,65,34], [45,23,23,45,65]]}
	 */
	var processDataMap = function(dataMap) {
		// assign data values to plot over time
		var dataValues = getRequiredVar(dataMap, 'values', "The data object must contain a 'values' value with a data array.")
		var startTime = new Date(getRequiredVar(dataMap, 'start', "The data object must contain a 'start' value with the start time in milliseconds since epoch."))
		var endTime = new Date(getRequiredVar(dataMap, 'end', "The data object must contain an 'end' value with the end time in milliseconds since epoch."))
		var step = getRequiredVar(dataMap, 'step', "The data object must contain a 'step' value with the time in milliseconds between each data value.")		
		var names = getRequiredVar(dataMap, 'names', "The data object must contain a 'names' array with the same length as 'values' with a name for each data value array.")		
		var displayNames = getOptionalVar(dataMap, 'displayNames', names);
		var numAxisLabelsPowerScale = getOptionalVar(dataMap, 'numAxisLabelsPowerScale', 6); 
		var numAxisLabelsLinearScale = getOptionalVar(dataMap, 'numAxisLabelsLinearScale', 6); 
		
		var axis = getOptionalVar(dataMap, 'axis', []);
		// default axis values
		if(axis.length == 0) {
			displayNames.forEach(function (v, i) {
				// set the default to left axis
				axis[i] = "left";
			})
		} else {
			var hasRightAxis = false;
			axis.forEach(function(v) {
				if(v == 'right') {
					hasRightAxis = true;
				}
			})
			if(hasRightAxis) {
				// add space to right margin
				margin[1] = margin[1] + 50;
			}
		}

		
		var colors = getOptionalVar(dataMap, 'colors', []);
		// default colors values
		if(colors.length == 0) {
			displayNames.forEach(function (v, i) {
				// set the default
				colors[i] = "black";
			})
		}
		
		var maxValues = [];
		var rounding = getOptionalVar(dataMap, 'rounding', []);
		// default rounding values
		if(rounding.length == 0) {
			displayNames.forEach(function (v, i) {
				// set the default to 0 decimals
				rounding[i] = 0;
			})
		}
		
		/* copy the dataValues array, do NOT assign the reference otherwise we modify the original source when we shift/push data */
		var newDataValues = [];
		dataValues.forEach(function (v, i) {
			newDataValues[i] = v.slice(0);
			maxValues[i] = d3.max(newDataValues[i])
		})

		

		
		return {
			"values" : newDataValues,
			"startTime" : startTime,
			"endTime" : endTime,
			"step" : step,
			"names" : names,
			"displayNames": displayNames,
			"axis" : axis,
			"colors": colors,
			"scale" : getOptionalVar(dataMap, 'scale', yScale),
			"maxValues" : maxValues,
			"rounding" : rounding,
			"numAxisLabelsLinearScale": numAxisLabelsLinearScale,
			"numAxisLabelsPowerScale": numAxisLabelsPowerScale
		}
	}
	
	var redrawAxes = function(withTransition) {
		initY();
		initX();
		
		if(withTransition) {
			// slide x-axis to updated location
			graph.selectAll("g .x.axis").transition()
			.duration(transitionDuration)
			.ease("linear")
			.call(xAxis)				  
		
			// slide y-axis to updated location
			graph.selectAll("g .y.axis.left").transition()
			.duration(transitionDuration)
			.ease("linear")
			.call(yAxisLeft)
			
			if(yAxisRight != undefined) {
				// slide y-axis to updated location
				graph.selectAll("g .y.axis.right").transition()
				.duration(transitionDuration)
				.ease("linear")
				.call(yAxisRight)
			}
		} else {
			// slide x-axis to updated location
			graph.selectAll("g .x.axis")
			.call(xAxis)				  
		
			// slide y-axis to updated location
			graph.selectAll("g .y.axis.left")
			.call(yAxisLeft)

			if(yAxisRight != undefined) {			
				// slide y-axis to updated location
				graph.selectAll("g .y.axis.right")
				.call(yAxisRight)
			}
		}
	}
	
	var redrawLines = function(withTransition) {
		/**
		* This is a hack to deal with the left/right axis.
		*
		* See createGraph for a larger comment explaining this. 
		*
		* Yes, it's ugly. If you can suggest a better solution please do.
		*/
		lineFunctionSeriesIndex  =-1;
		
		// redraw lines
		if(withTransition) {
			graph.selectAll("g .lines path")
			.transition()
				.duration(transitionDuration)
				.ease("linear")
				.attr("d", lineFunction)
				.attr("transform", null);
		} else {
			graph.selectAll("g .lines path")
				.attr("d", lineFunction)
				.attr("transform", null);
		}
	}
	
	/*
	 * Allow re-initializing the y function at any time.
	 *  - it will properly determine what scale is being used based on last user choice (via public switchScale methods)
	 */
	var initY = function() {
		initYleft();
		initYright();
	}
	
	var initYleft = function() {
		var maxYscaleLeft = calculateMaxY(data, 'left')
		//debug("initY => maxYscale: " + maxYscaleLeft);
		var numAxisLabels = 6;
		if(yScale == 'pow') {
			yLeft = d3.scale.pow().exponent(0.3).domain([0, maxYscaleLeft]).range([h, 0]).nice();	
			numAxisLabels = data.numAxisLabelsPowerScale;
		} else if(yScale == 'log') {
			// we can't have 0 so will represent 0 with a very small number
			// 0.1 works to represent 0, 0.01 breaks the tickFormatter
			yLeft = d3.scale.log().domain([0.1, maxYscaleLeft]).range([h, 0]).nice();	
		} else if(yScale == 'linear') {
			yLeft = d3.scale.linear().domain([0, maxYscaleLeft]).range([h, 0]).nice();
			numAxisLabels = data.numAxisLabelsLinearScale;
		}

		yAxisLeft = d3.svg.axis().scale(yLeft).ticks(numAxisLabels, tickFormatForLogScale).orient("left");
	}
	
	var initYright = function() {
		var maxYscaleRight = calculateMaxY(data, 'right')
		// only create the right axis if it has values
		if(maxYscaleRight != undefined) {
			//debug("initY => maxYscale: " + maxYscaleRight);
			var numAxisLabels = 6;
			if(yScale == 'pow') {
				yRight = d3.scale.pow().exponent(0.3).domain([0, maxYscaleRight]).range([h, 0]).nice();		
				numAxisLabels = data.numAxisLabelsPowerScale;
			} else if(yScale == 'log') {
				// we can't have 0 so will represent 0 with a very small number
				// 0.1 works to represent 0, 0.01 breaks the tickFormatter
				yRight = d3.scale.log().domain([0.1, maxYscaleRight]).range([h, 0]).nice();	
			} else if(yScale == 'linear') {
				yRight = d3.scale.linear().domain([0, maxYscaleRight]).range([h, 0]).nice();
				numAxisLabels = data.numAxisLabelsLinearScale;
			}
			
			yAxisRight = d3.svg.axis().scale(yRight).ticks(numAxisLabels, tickFormatForLogScale).orient("right");
		}
	}
	
	
	
	/*
	 * Whenever we add/update data we want to re-calculate if the max Y scale has changed
	 */
	var calculateMaxY = function(data, whichAxis) {
		// Y scale will fit values from 0-10 within pixels h-0 (Note the inverted domain for the y-scale: bigger is up!)
			// we get the max of the max of values for the given index since we expect an array of arrays

		// we can shortcut to using data.maxValues since we've already calculated the max of each series in processDataMap

		var maxValuesForAxis = [];
		data.maxValues.forEach(function(v, i) {
			if(data.axis[i] == whichAxis) {
				maxValuesForAxis.push(v);
			}
		})
		
		// we now have the max values for the axis we're interested in so get the max of them
		return d3.max(maxValuesForAxis);
	}
	
	/*
	 * Allow re-initializing the x function at any time.
	 */
	var initX = function() {
		// X scale starts at epoch time 1335035400000, ends at 1335294600000 with 300s increments
		x = d3.time.scale().domain([data.startTime, data.endTime]).range([0, w]);
		
		// create yAxis (with ticks)
		xAxis = d3.svg.axis().scale(x).tickSize(-h).tickSubdivide(1);
			// without ticks
			//xAxis = d3.svg.axis().scale(x);
	}

	/**
	* Creates the SVG elements and displays the line graph.
	*
	* Expects to be called once during instance initialization.
	*/
	var createGraph = function() {
		
		// Add an SVG element with the desired dimensions and margin.
		graph = d3.select("#" + containerId).append("svg:svg")
				.attr("class", "line-graph")
				.attr("width", w + margin[1] + margin[3])
				.attr("height", h + margin[0] + margin[2])	
				.append("svg:g")
					.attr("transform", "translate(" + margin[3] + "," + margin[0] + ")");

		initX()		
		
		// Add the x-axis.
		graph.append("svg:g")
			.attr("class", "x axis")
			.attr("transform", "translate(0," + h + ")")
			.call(xAxis);
			
		
		// y is all done in initY because we need to re-assign vars quite often to change scales
		initY();
				
		// Add the y-axis to the left
		graph.append("svg:g")
			.attr("class", "y axis left")
			.attr("transform", "translate(-10,0)")
			.call(yAxisLeft);
			
		if(yAxisRight != undefined) {
			// Add the y-axis to the right if we need one
			graph.append("svg:g")
				.attr("class", "y axis right")
				.attr("transform", "translate(" + (w+10) + ",0)")
				.call(yAxisRight);
		}
				
		// create line function used to plot our data
		lineFunction = d3.svg.line()
			// assign the X function to plot our line as we wish
			.x(function(d,i) { 
				/* 
				 * Our x value is defined by time and since our data doesn't have per-metric timestamps
				 * we calculate time as (startTime + the step between metrics * the index)
				 *
 				 * We also reach out to the persisted 'data' object for time
 				 * since the 'd' passed in here is one of the children, not the parent object
				 */
				var _x = x(data.startTime.getTime() + (data.step*i)); 
				
				// verbose logging to show what's actually being done
				//debug("Line X => index: " + i + " scale: " + _x)
				// return the X coordinate where we want to plot this datapoint
				return _x;
			})
			.y(function(d, i) { 
				if(yScale == 'log' && d < 0.1) {
					// log scale can't have 0s, so we set it to the smallest value we set on y
					d = 0.1;
				}
				
				/**
				* This is a hack that relies on:
				*   a) the single-threaded nature of javascript that this will not be interleaved
				*   b) that lineFunction will always be passed the data[] for all lines in the same way each time
				*
				* We then use an external variable to track each time we move from one series to the next
				* so that we can have its seriesIndex to access information in the data[] object, particularly
				* so we can determine what axis this data is supposed to be on.
				*
				* I didn't want to split the line function into left and right lineFunctions as that would really
				* complicate the data binding.
				*
				* Also ... I can't figure out nested functions to keep it scoped so I had to put lineFunctionSeriesIndex
				* as a variable in the same scope as lineFunction. Ugly. And worse ... reset it in redrawAxes. 
				*
				* Anyone reading this who knows a better solution please let me know.
				*/
				if(i == 0) {
					lineFunctionSeriesIndex++;
				}
				var axis = data.axis[lineFunctionSeriesIndex];
				var _y;
				if(axis == 'right') {
					_y = yRight(d); 
				} else {
					_y = yLeft(d); 
				}

				// verbose logging to show what's actually being done
				//debug("Line Y => data: " + d + " scale: " + _y)
				// return the Y coordinate where we want to plot this datapoint
				return _y;
			})
			.defined(function(d) {
				// handle missing data gracefully
				// feature added in https://github.com/mbostock/d3/pull/594
				return d >= 0;
			});

		// append a group to contain all lines
		lines = graph.append("svg:g")
				.attr("class", "lines")
			.selectAll("path")
				.data(data.values); // bind the array of arrays

		// persist this reference so we don't do the selector every mouse event
		hoverContainer = container.querySelector('g .lines');
		
		
		$(container).mouseleave(function(event) {
			handleMouseOutGraph(event);
		})
		
		$(container).mousemove(function(event) {
			handleMouseOverGraph(event);
		})		

					
		// add a line group for each array of values (it will iterate the array of arrays bound to the data function above)
		linesGroup = lines.enter().append("g")
				.attr("class", function(d, i) {
					return "line_group series_" + i;
				});
				
		// add path (the actual line) to line group
		linesGroup.append("path")
				.attr("class", function(d, i) {
					//debug("Appending line [" + containerId + "]: " + i)
					return "line series_" + i;
				})
				.attr("fill", "none")
				.attr("stroke", function(d, i) {
					return data.colors[i];
				})
				.attr("d", lineFunction) // use the 'lineFunction' to create the data points in the correct x,y axis
				.on('mouseover', function(d, i) {
					handleMouseOverLine(d, i);
				});
				
		// add line label to line group
		linesGroupText = linesGroup.append("svg:text");
		linesGroupText.attr("class", function(d, i) {
				//debug("Appending line [" + containerId + "]: " + i)
				return "line_label series_" + i;
			})
			.text(function(d, i) {
				return "";
			});
			
		// add a 'hover' line that we'll show as a user moves their mouse (or finger)
		// so we can use it to show detailed values of each line
		hoverLineGroup = graph.append("svg:g")
							.attr("class", "hover-line");
		// add the line to the group
		hoverLine = hoverLineGroup
			.append("svg:line")
				.attr("x1", 10).attr("x2", 10) // vertical line so same value on each
				.attr("y1", 0).attr("y2", h); // top to bottom	
				
		// hide it by default
		hoverLine.classed("hide", true);
			
		createScaleButtons();
		createDateLabel();
		createLegend();		
		setValueLabelsToLatest();
	}
	
	/**
	 * Create a legend that displays the name of each line with appropriate color coding
	 * and allows for showing the current value when doing a mouseOver
	 */
	var createLegend = function() {
		
		// append a group to contain all lines
		var legendLabelGroup = graph.append("svg:g")
				.attr("class", "legend-group")
			.selectAll("g")
				.data(data.displayNames)
			.enter().append("g")
				.attr("class", "legend-labels");
				
		legendLabelGroup.append("svg:text")
				.attr("class", "legend name")
				.text(function(d, i) {
					return d;
				})
				.attr("font-size", legendFontSize)
				.attr("fill", function(d, i) {
					// return the color for this row
					return data.colors[i];
				})
				.attr("y", function(d, i) {
					return h+28;
				})

				
		// put in placeholders with 0 width that we'll populate and resize dynamically
		legendLabelGroup.append("svg:text")
				.attr("class", "legend value")
				.attr("font-size", legendFontSize)
				.attr("fill", function(d, i) {
					return data.colors[i];
				})
				.attr("y", function(d, i) {
					return h+28;
				})
				
		// x values are not defined here since those get dynamically calculated when data is set in displayValueLabelsForPositionX()
	}
	
	var redrawLegendPosition = function(animate) {
		var legendText = graph.selectAll('g.legend-group text');
		if(animate) {
			legendText.transition()
			.duration(transitionDuration)
			.ease("linear")
			.attr("y", function(d, i) {
				return h+28;
			});	
			
		} else {
			legendText.attr("y", function(d, i) {
				return h+28;
			});	
		}	
	}
	
	/**
	 * Create scale buttons for switching the y-axis
	 */
	var createScaleButtons = function() {
		var cumulativeWidth = 0;		
		// append a group to contain all lines
		var buttonGroup = graph.append("svg:g")
				.attr("class", "scale-button-group")
			.selectAll("g")
				.data(scales)
			.enter().append("g")
				.attr("class", "scale-buttons")
			.append("svg:text")
				.attr("class", "scale-button")
				.text(function(d, i) {
					return d[1];
				})
				.attr("font-size", "12") // this must be before "x" which dynamically determines width
				.attr("fill", function(d) {
					if(d[0] == yScale) {
						return "black";
					} else {
						return "blue";
					}
				})
				.classed("selected", function(d) {
					if(d[0] == yScale) {
						return true;
					} else {
						return false;
					}
				})
				.attr("x", function(d, i) {
					// return it at the width of previous labels (where the last one ends)
					var returnX = cumulativeWidth;
					// increment cumulative to include this one
					cumulativeWidth += this.getComputedTextLength()+5;
					return returnX;
				})
				.attr("y", -4)
				.on('click', function(d, i) {
					handleMouseClickScaleButton(this, d, i);
				});
	}

	var handleMouseClickScaleButton = function(button, buttonData, index) {
		if(index == 0) {
			self.switchToLinearScale();
		} else if(index == 1) {
			self.switchToPowerScale();
		} else if(index == 2) {
			self.switchToLogScale();
		}
		
		// change text decoration
		graph.selectAll('.scale-button')
		.attr("fill", function(d) {
			if(d[0] == yScale) {
				return "black";
			} else {
				return "blue";
			}
		})
		.classed("selected", function(d) {
			if(d[0] == yScale) {
				return true;
			} else {
				return false;
			}
		})
		
	}
	
	/**
	 * Create a data label
	 */
	var createDateLabel = function() {
		var date = new Date(); // placeholder just so we can calculate a valid width
		// create the date label to the left of the scaleButtons group
		var buttonGroup = graph.append("svg:g")
				.attr("class", "date-label-group")
			.append("svg:text")
				.attr("class", "date-label")
				.attr("text-anchor", "end") // set at end so we can position at far right edge and add text from right to left
				.attr("font-size", "10") 
				.attr("y", -4)
				.attr("x", w)
				.text(date.toDateString() + " " + date.toLocaleTimeString())
				
	}

	
	/**
	 * Called when a user mouses over a line.
	 */
	var handleMouseOverLine = function(lineData, index) {
		//debug("MouseOver line [" + containerId + "] => " + index)
		
		// user is interacting
		userCurrentlyInteracting = true;
	}

	/**
	 * Called when a user mouses over the graph.
	 */
	var handleMouseOverGraph = function(event) {	
		var mouseX = event.pageX-hoverLineXOffset;
		var mouseY = event.pageY-hoverLineYOffset;
		
		//debug("MouseOver graph [" + containerId + "] => x: " + mouseX + " y: " + mouseY + "  height: " + h + " event.clientY: " + event.clientY + " offsetY: " + event.offsetY + " pageY: " + event.pageY + " hoverLineYOffset: " + hoverLineYOffset)
		if(mouseX >= 0 && mouseX <= w && mouseY >= 0 && mouseY <= h) {
			// show the hover line
			hoverLine.classed("hide", false);

			// set position of hoverLine
			hoverLine.attr("x1", mouseX).attr("x2", mouseX)
			
			displayValueLabelsForPositionX(mouseX)
			
			// user is interacting
			userCurrentlyInteracting = true;
			currentUserPositionX = mouseX;
		} else {
			// proactively act as if we've left the area since we're out of the bounds we want
			handleMouseOutGraph(event)
		}
	}
	
	
	var handleMouseOutGraph = function(event) {	
		// hide the hover-line
		hoverLine.classed("hide", true);
		
		setValueLabelsToLatest();
		
		//debug("MouseOut graph [" + containerId + "] => " + mouseX + ", " + mouseY)
		
		// user is no longer interacting
		userCurrentlyInteracting = false;
		currentUserPositionX = -1;
	}
	
/*  // if we need to support older browsers without pageX/pageY we can use this
	var getMousePositionFromEvent = function(e, element) {
		var posx = 0;
		var posy = 0;
		if (!e) var e = window.event;
		if (e.pageX || e.pageY) 	{
			posx = e.pageX;
			posy = e.pageY;
		}
		else if (e.clientX || e.clientY) 	{
			posx = e.clientX + document.body.scrollLeft
				+ document.documentElement.scrollLeft;
			posy = e.clientY + document.body.scrollTop
				+ document.documentElement.scrollTop;
		}
		
		return {x: posx, y: posy};
	}
*/
	
	/*
	* Handler for when data is updated.
	*/
	var handleDataUpdate = function() {
		if(userCurrentlyInteracting) {
			// user is interacting, so let's update values to wherever the mouse/finger is on the updated data
			if(currentUserPositionX > -1) {
				displayValueLabelsForPositionX(currentUserPositionX)
			}
		} else {
			// the user is not interacting with the graph, so we'll update the labels to the latest
			setValueLabelsToLatest();
		}
	}
	
	/**
	* Display the data values at position X in the legend value labels.
	*/
	var displayValueLabelsForPositionX = function(xPosition, withTransition) {
		var animate = false;
		if(withTransition != undefined) {
			if(withTransition) {
				animate = true;
			}
		}
		var dateToShow;
		var labelValueWidths = [];
		graph.selectAll("text.legend.value")
		.text(function(d, i) {
			var valuesForX = getValueForPositionXFromData(xPosition, i);
			dateToShow = valuesForX.date;
			return valuesForX.value;
		})
		.attr("x", function(d, i) {
			labelValueWidths[i] = this.getComputedTextLength();
		})

		// position label names
		var cumulativeWidth = 0;
		var labelNameEnd = [];
		graph.selectAll("text.legend.name")
				.attr("x", function(d, i) {
					// return it at the width of previous labels (where the last one ends)
					var returnX = cumulativeWidth;
					// increment cumulative to include this one + the value label at this index
					cumulativeWidth += this.getComputedTextLength()+4+labelValueWidths[i]+8;
					// store where this ends
					labelNameEnd[i] = returnX + this.getComputedTextLength()+5;
					return returnX;
				})

		// remove last bit of padding from cumulativeWidth
		cumulativeWidth = cumulativeWidth - 8;

		if(cumulativeWidth > w) {
			// decrease font-size to make fit
			legendFontSize = legendFontSize-1;
			//debug("making legend fit by decreasing font size to: " + legendFontSize)
			graph.selectAll("text.legend.name")
				.attr("font-size", legendFontSize);
			graph.selectAll("text.legend.value")
				.attr("font-size", legendFontSize);
			
			// recursively call until we get ourselves fitting
			displayValueLabelsForPositionX(xPosition);
			return;
		}

		// position label values
		graph.selectAll("text.legend.value")
		.attr("x", function(d, i) {
			return labelNameEnd[i];
		})
		

		// show the date
		graph.select('text.date-label').text(dateToShow.toDateString() + " " + dateToShow.toLocaleTimeString())

		// move the group of labels to the right side
		if(animate) {
			graph.selectAll("g.legend-group g")
				.transition()
				.duration(transitionDuration)
				.ease("linear")
				.attr("transform", "translate(" + (w-cumulativeWidth) +",0)")
		} else {
			graph.selectAll("g.legend-group g")
				.attr("transform", "translate(" + (w-cumulativeWidth) +",0)")
		}
	}
	
	/**
	* Set the value labels to whatever the latest data point is.
	*/
	var setValueLabelsToLatest = function(withTransition) {
		displayValueLabelsForPositionX(w, withTransition);
	}
	
	/**
	* Convert back from an X position on the graph to a data value from the given array (one of the lines)
	* Return {value: value, date, date}
	*/
	var getValueForPositionXFromData = function(xPosition, dataSeriesIndex) {
		var d = data.values[dataSeriesIndex]
		
		// get the date on x-axis for the current location
		var xValue = x.invert(xPosition);

		// Calculate the value from this date by determining the 'index'
		// within the data array that applies to this value
		var index = (xValue.getTime() - data.startTime) / data.step;


		if(index >= d.length) {
			index = d.length-1;
		}
		// The date we're given is interpolated so we have to round off to get the nearest
		// index in the data array for the xValue we're given.
		// Once we have the index, we then retrieve the data from the d[] array
		index = Math.round(index);

		// bucketDate is the date rounded to the correct 'step' instead of interpolated
		var bucketDate = new Date(data.startTime.getTime() + data.step * (index+1)); // index+1 as it is 0 based but we need 1-based for this math
				
		var v = d[index];

		var roundToNumDecimals = data.rounding[dataSeriesIndex];

		return {value: roundNumber(v, roundToNumDecimals), date: bucketDate};
	}

	
	/**
	 * Called when the window is resized to redraw graph accordingly.
	 */
	var handleWindowResizeEvent = function() {
	 	//debug("Window Resize Event [" + containerId + "] => resizing graph")
	 	initDimensions();
		initX();
		
		// reset width/height of SVG
		d3.select("#" + containerId + " svg")
				.attr("width", w + margin[1] + margin[3])
				.attr("height", h + margin[0] + margin[2]);

		// reset transform of x axis
		graph.selectAll("g .x.axis")
			.attr("transform", "translate(0," + h + ")");
			
		if(yAxisRight != undefined) {
			// Reset the y-axisRight transform if it exists
			graph.selectAll("g .y.axis.right")
				.attr("transform", "translate(" + (w+10) + ",0)");
		}

		// reset legendFontSize on window resize so it has a chance to re-calculate to a bigger size if it can now fit 
		legendFontSize = 12;
		//debug("making legend fit by decreasing font size to: " + legendFontSize)
		graph.selectAll("text.legend.name")
			.attr("font-size", legendFontSize);
		graph.selectAll("text.legend.value")
			.attr("font-size", legendFontSize);

		// move date label
		graph.select('text.date-label')
			.transition()
			.duration(transitionDuration)
			.ease("linear")
			.attr("x", w)

		// redraw the graph with new dimensions
		redrawAxes(true);
		redrawLines(true);
		
		// reposition legend if necessary
		redrawLegendPosition(true);
				
		// force legend to redraw values
		setValueLabelsToLatest(true);
	}

	/**
	 * Set height/width dimensions based on container.
	 */
	var initDimensions = function() {
		// automatically size to the container using JQuery to get width/height
		w = $("#" + containerId).width() - margin[1] - margin[3]; // width
		h = $("#" + containerId).height() - margin[0] - margin[2]; // height
		
		// make sure to use offset() and not position() as we want it relative to the document, not its parent
		hoverLineXOffset = margin[3]+$(container).offset().left;
		hoverLineYOffset = margin[0]+$(container).offset().top;
	}
	
	/**
	* Return the value from argsMap for key or throw error if no value found
	*/	  
	var getRequiredVar = function(argsMap, key, message) {
		if(!argsMap[key]) {
			if(!message) {
				throw new Error(key + " is required")
			} else {
				throw new Error(message)
			}
		} else {
			return argsMap[key]
		}
	}
	
	/**
	* Return the value from argsMap for key or defaultValue if no value found
	*/
	var getOptionalVar = function(argsMap, key, defaultValue) {
		if(!argsMap[key]) {
			return defaultValue
		} else {
			return argsMap[key]
		}
	}
	
	var error = function(message) {
		console.log("ERROR: " + message)
	}

	var debug = function(message) {
		console.log("DEBUG: " + message)
	}
	
	/* round a number to X digits: num => the number to round, dec => the number of decimals */
	/* private */ function roundNumber(num, dec) {
		var result = Math.round(num*Math.pow(10,dec))/Math.pow(10,dec);
		var resultAsString = result.toString();
		if(dec > 0) {
			if(resultAsString.indexOf('.') == -1) {
				resultAsString = resultAsString + '.';
			}
			// make sure we have a decimal and pad with 0s to match the number we were asked for
			var indexOfDecimal = resultAsString.indexOf('.');
			while(resultAsString.length <= (indexOfDecimal+dec)) {
				resultAsString = resultAsString + '0';
			}
		}
		return resultAsString;
	};
	
	/* *************************************************************** */
	/* execute init now that everything is defined */
	/* *************************************************************** */
	_init();
};
